# -*- coding: utf-8 -*-
require "jdbc_migration"
module JdbcMigration
  class MetaData
    JDBC_TYPE_TO_RAILS_TYPE = {
      # "ARRAY" => :unknown,
      "BIGINT" => :integer,
      "BINARY" => :binary,
      "BIT" => :boolean,
      "BLOB" => :binary,
      "BOOLEAN" => :boolean,
      "CHAR" => :string,
      "CLOB" => :text,
      # "DATALINK" => :unknown,
      "DATE" => :date,
      "DECIMAL" => :decimal,
      # "DISTINCT" => :unknown,
      "DOUBLE" => :float,
      # "Deprecated" => :unknown,
      "FLOAT" => :float,
      "INTEGER" => :integer,
      # "JAVA_OBJECT" => :unknown,
      # "JavaSignal" => :unknown,
      "LONGNVARCHAR" => :text,
      "LONGVARBINARY" => :binary,
      "LONGVARCHAR" => :text,
      "NCHAR" => :string,
      "NCLOB" => :text,
      # "NULL" => :unknown,
      "NUMERIC" => :float,
      "NVARCHAR" => :string,
      # "OTHER" => :unknown,
      "REAL" => :float,
      # "REF" => :unknown,
      "ROWID" => :integer,
      # "SIGNALS" => :unknown,
      "SMALLINT" => :integer,
      # "SQLXML" => :unknown,
      # "STRUCT" => :unknown,
      "TIME" => :time,
      "TIMESTAMP" => :timestamp,
      "TINYINT" => :integer,
      "VARBINARY" => :binary,
      "VARCHAR" => :string
    }
    
    TYPE_ID_TO_NAMES = java.sql.Types.constants.inject({}) do |dest, name|
      value = java.sql.Types.const_get(name)
      dest[name] = value
      dest[name.downcase] = value
      dest
    end
    
    #    md = java.sql.DatabaseMetaData
    #    NULLABLE_SYMBOLS = {
    #      #         * columnNoNulls         - NULL 値を許さない可能性がある
    #      #         * columnNullable        - 必ず NULL 値を許す
    #      #         * columnNullableUnknown - NULL 値を許すかどうかは不明 
    #      java.sql.DatabaseMetaData::columnNoNulls => false,
    #      java.sql.DatabaseMetaData::columnNullable => true,
    #      java.sql.DatabaseMetaData::columnNullableUnknown => nil
    #    }
    
    class MetaElement
      attr_reader :meta_data, :jdbc_info
      def initialize(meta_data, options)
        @meta_data = meta_data
        @jdbc_info = options.dup
        options.each do |attr, value|
          m = "#{attr.downcase}="
          self.send(m, value) if respond_to?(m)
        end
      end
      
      def pretty_print_instance_variables
        self.instance_variables.sort.map(&:to_sym) - [:@meta_data, :@jdbc_info, :@table]
      end
    end
    
    class Table < MetaElement
      #  1. TABLE_CAT       String => テーブルカタログ (null の可能性がある)
      #  2. TABLE_SCHEM     String => テーブルスキーマ (null の可能性がある)
      #  3. TABLE_NAME      String => テーブル名
      #  4. TABLE_TYPE      String => テーブルの型。典型的な型は、"TABLE"、"VIEW"、"SYSTEM TABLE"、"GLOBAL TEMPORARY"、"LOCAL TEMPORARY"、"ALIAS"、"SYNONYM" である
      #  5. REMARKS         String => テーブルに関する説明
      #  6. TYPE_CAT        String => の型のカタログ (null の可能性がある)
      #  7. TYPE_SCHEM      String => の型のスキーマ (null の可能性がある)
      #  8. TYPE_NAME       String => の型名 (null の可能性がある)
      #  9. SELF_REFERENCING_COL_NAME String => 型付きテーブルの指定された「識別子」列の名前 (null の可能性がある)
      # 10. REF_GENERATION  String => SELF_REFERENCING_COL_NAME の値の作成方法を指定する。値は、"SYSTEM"、"USER"、"DERIVED" (null の可能性がある) 
      attr_accessor :table_cat, :table_schem, :table_name, 
      :table_type,
      :remarks, :type_cat, :type_schem, :type_name, 
      :self_referencing_col_name, :ref_generation
      alias_method :name, :table_name
      
      attr_accessor :pluralize_table_name
      attr_accessor :columns
      attr_accessor :primary_keys
      attr_accessor :imported_keys
      attr_accessor :exported_keys
      
      def primary_key
        primary_keys.nil? ? nil :
          primary_keys.empty? ? nil : 
          primary_keys.length == 1 ? primary_keys.first : nil
      end
      
      MATCHING_ATTRS = %w(TABLE_CAT TABLE_SCHEM TABLE_NAME)
      
      def match?(meta_data)
        MATCHING_ATTRS.all?{|attr|jdbc_info[attr] == meta_data[attr]}
      end
      
      def rails_table_name
        unless @rails_table_name
          @rails_table_name = table_name.downcase
          @rails_table_name = @rails_table_name.pluralize if pluralize_table_name
        end
        @rails_table_name
      end
      
      def rails_table_name=(value)
        @rails_table_name = value
      end
      attr_accessor :rails_options
      
      def define_rails_model(mod)
        class_name = "Rails#{rails_table_name.classify}"
        class_def =  "class #{class_name} < ActiveRecord::Base\n"
        class_def << "  set_table_name('#{rails_table_name}')\n"
        class_def << "end"
        mod.module_eval(class_def)
        mod.const_get(class_name)
      end
      
      def define_jdbc_model(mod)
        class_name = "Jdbc#{table_name.classify}"
        class_def =  "class #{class_name} < ActiveRecord::Base\n"
        class_def << "  set_table_name('#{table_name}')\n"
        class_def << "  establish_connection('#{meta_data.source_db}')\n"
        class_def << "end"
        mod.module_eval(class_def)
        mod.const_get(class_name)
      end
      
      def [](column_name)
        column_name = column_name.to_s.upcase
        columns.detect{|col|col.name == column_name}
      end
    end
    
    class Column < MetaElement
      #  1. TABLE_CAT           String => テーブルカタログ (null の可能性がある)
      #  2. TABLE_SCHEM         String => テーブルスキーマ (null の可能性がある)
      #  3. TABLE_NAME          String => テーブル名
      #  4. COLUMN_NAME         String => 列名
      #  5. DATA_TYPE           short => java.sql.Types からの SQL の型
      #  6. TYPE_NAME           String => データソース依存の型名。UDT の場合、型名は完全指定
      #  7. COLUMN_SIZE         int => 列サイズ。char や date の型については最大文字数、numeric や decimal の型については精度
      #  8. BUFFER_LENGTH        - 未使用
      #  9. DECIMAL_DIGITS      int => 小数点以下の桁数
      # 10. NUM_PREC_RADIX      int => 基数 (通常は、10 または 2 のどちらか)
      # 11. NULLABLE            int => NULL は許されるか
      #         * columnNoNulls         - NULL 値を許さない可能性がある
      #         * columnNullable        - 必ず NULL 値を許す
      #         * columnNullableUnknown - NULL 値を許すかどうかは不明 
      # 12. REMARKS             String => コメント記述列 (null の可能性がある)
      # 13. COLUMN_DEF          String => デフォルト値 (null の可能性がある)
      # 14. SQL_DATA_TYPE       int => 未使用
      # 15. SQL_DATETIME_SUB    int => 未使用
      # 16. CHAR_OCTET_LENGTH   int => char の型については列の最大バイト数
      # 17. ORDINAL_POSITION    int => テーブル中の列のインデックス (1 から始まる)
      # 18. IS_NULLABLE         String => "NO" は、列は決して NULL 値を許さないことを意味する。"YES" は NULL 値を許す可能性があることを意味する。空の文字列は不明であることを意味する
      # 19. SCOPE_CATLOG        String => 参照属性のスコープであるテーブルのカタログ (DATA_TYPE が REF でない場合は null)
      # 20. SCOPE_SCHEMA        String => 参照属性のスコープであるテーブルのスキーマ (DATA_TYPE が REF でない場合は null)
      # 21. SCOPE_TABLE         String => 参照属性のスコープであるテーブル名 (DATA_TYPE が REF でない場合は null)
      # 22. SOURCE_DATA_TYPE    short => 個別の型またはユーザ生成 Ref 型、java.sql.Types の SQL 型のソースの型 (DATA_TYPE が DISTINCT またはユーザ生成 REF でない場合は null) 
      attr_accessor :table_cat, :table_schem, :table_name, 
      :column_name, :data_type, :type_name, :column_size,
      :buffer_length, :decimal_digits, :num_prec_radix,
      :nullable, :remarks, :column_def, :sql_data_type,
      :sql_datetime_sub, :char_octet_length,
      :ordinal_position, :is_nullable, 
      :scope_catlog, :scope_schema, :scope_table, :scope_data_type
      alias_method :name, :column_name
      alias_method :size, :column_size
      
      attr_reader :table
      
      def initialize(meta_data, table, *args, &block)
        super(meta_data, *args, &block)
        @table = table
      end
      
      def jdbc_type
        @column_type ||= (TYPE_ID_TO_NAMES[data_type] || type_name || '')
      end
      
      def rails_type
        @rails_type ||= (
          (table.primary_key != self.name) ? JDBC_TYPE_TO_RAILS_TYPE[jdbc_type] :
          (/^id$/ =~ self.name) ? nil : :primary_key
          )
      end
      
      def rails_type=(value)
        @rails_type = value
      end

      attr_accessor :rails_ignored
      attr_accessor :rails_name, :rails_options
      attr_reader :name_changed?
      
      def nullable?
        @_nullable ||= (is_nullable != 'NO')
      end
      
      def primary_key_index
        @primary_key_index ||= table.primary_keys.index(self.name)
      end
      
      def primary_key?
        !!primary_key_index
      end
      
      def default
        self.column_def.nil? ? nil :
          /^NULL$/i =~ self.column_def.to_s ? nil : self.column_def
      end
    end
    
    class Key < MetaElement
      #   1. PKTABLE_CAT String => インポートされた主キーテーブルカタログ (null の可能性がある)
      #   2. PKTABLE_SCHEM String => インポートされた主キーテーブルスキーマ (null の可能性がある)
      #   3. PKTABLE_NAME String => インポートされた主キーテーブル名
      #   4. PKCOLUMN_NAME String => インポートされた主キー列名
      #   5. FKTABLE_CAT String => 外部キーテーブルカタログ (null の可能性がある)
      #   6. FKTABLE_SCHEM String => 外部キーテーブルスキーマ (null の可能性がある)
      #   7. FKTABLE_NAME String => 外部キーテーブル名
      #   8. FKCOLUMN_NAME String => 外部キー列名
      #   9. KEY_SEQ short => 外部キー中の連番
      #  10. UPDATE_RULE short => 主キーが更新されるときに、外部キーに起こる内容は次のとおりである
      #          * importedNoAction - 主キーがインポートされたら、更新できない
      #          * importedKeyCascade - 主キーの更新に合致するように、インポートされたキーを変更する
      #          * importedKeySetNull - インポートされたキーの主キーが更新されたら、NULL に変更する
      #          * importedKeySetDefault - インポートされたキーの主キーが更新されたら、デフォルト値に変更する
      #          * importedKeyRestrict - importedKeyNoAction と同じ (ODBC 2.x との互換性のため) 
      #  11. DELETE_RULE short => 主キーが削除されると、外部キーに起こる内容は次のとおりである
      #          * importedKeyNoAction - 主キーがインポートされたら、削除できない
      #          * importedKeyCascade - 削除されたキーをインポートする行を、削除する
      #          * importedKeySetNull - インポートされたキーの主キーが削除されたら、NULL に変更する
      #          * importedKeyRestrict - importedKeyNoAction と同じ (ODBC 2.x との互換性のため)
      #          * importedKeySetDefault - インポートされたキーの主キーが削除されたら、デフォルト値に変更する 
      #  12. FK_NAME String => 外部キー名 (null の可能性がある)
      #  13. PK_NAME String => 主キー名 (null の可能性がある)
      #  14. DEFERRABILITY short => 外部キーの制限の評価はコミットまで延期できる
      #          * importedKeyInitiallyDeferred - 定義については SQL92 を参照
      #          * importedKeyInitiallyImmediate - 定義については SQL92 を参照
      #          * importedKeyNotDeferrable - 定義については SQL92 を参照 
      attr_accessor :pktable_cat, :pktable_schem, :pktable_name, :pkcolumn_name,
      :fktable_cat, :fktable_schem, :fktable_name, :fkcolumn_name,
      :key_seq, :update_rule, :delete_rule, :fk_name, :pk_name, :deferrability
      
      attr_reader :table
      alias_method :fktable, :table
      attr_accessor :pktable

      def initialize(meta_data, table, *args, &block)
        super(meta_data, *args, &block)
        @table = table
      end

      def pretty_print_instance_variables
        super - [:@table, :@fktable, :@pktable]
      end

    end
  end

  class MetaData
    attr_accessor :source_db
    def self.create(source_db, options = nil)
      @instances ||= {}
      result = @instances[source_db]
      unless result
        result = new(source_db, options)
        @instances[source_db] = result
      end
      result
    end
    
    def initialize(source_db, options = nil)
      if source_db.is_a?(Hash)
        raise ArgumentError, "Not implemented yet."
      else
        # db_config = ActiveRecord::Base.configurations[source_db.to_s]
        @source_db = source_db.to_s
        dummy_class_name = "#{@source_db.classify}Dummy"
        dummy_difinition =  "class #{dummy_class_name} < ActiveRecord::Base\n"
        dummy_difinition << "  establish_connection('#{@source_db}')\n"
        dummy_difinition << "end"
        MetaData.class_eval(dummy_difinition)

        dummy_klass = MetaData.const_get(dummy_class_name)
        c = dummy_klass.connection
        conn = c.instance_variable_get(:@connection).instance_variable_get(:@connection)
      end
      @jdbc_metadata = (options ? options[:meta_data] : nil) || conn.getMetaData
    rescue
      ActiveRecord::Base.logger.debug($!)
      ActiveRecord::Base.logger.debug($!.backtrace.join("\n  "))
      raise
    end
    
    def result_set_to_hash_array(rs)
      column_names = []
      md = rs.getMetaData
      md.getColumnCount.times do |idx|
        column_names << md.getColumnName(idx + 1)
      end
      result = []
      while rs.next
        result << column_names.inject({}) do |dest, column_name|
          dest[column_name] = rs.getObject(column_name)
          dest
        end
      end
      result
    end
    
    def tables(catalog, schema_pattern, table_name_pattern, options = nil)
      @tables ||= {}
      key = [catalog, schema_pattern, table_name_pattern]
      result = @tables[key]
      unless result
        md = @jdbc_metadata
        result = result_set_to_hash_array(md.getTables(catalog, schema_pattern, table_name_pattern, nil))
        result = result.map{|t|Table.new(self, t)}
        name_to_table = result.inject({}){|dest, table|dest[table.name] = table; dest}
        
        #  1. TABLE_CAT String => テーブルカタログ (null の可能性がある)
        #  2. TABLE_SCHEM String => テーブルスキーマ (null の可能性がある)
        #  3. TABLE_NAME String => テーブル名
        #  4. COLUMN_NAME String => 列名
        #  5. KEY_SEQ short => 主キー中の連番
        #  6. PK_NAME String => 主キー名 (null の可能性がある) 
        pkeys = result_set_to_hash_array(md.getPrimaryKeys(catalog, schema_pattern, table_name_pattern))
        result.each do |table|
          table.primary_keys = pkeys.
            select{|hash|table.match?(hash)}.
            sort{|a,b| a['KEY_SEQ'] <=> b['KEY_SEQ']}.
            map{|pk|pk['COLUMN_NAME']}
        end
        
        columns = result_set_to_hash_array(md.getColumns(catalog, schema_pattern, table_name_pattern, nil))
        result.each do |table|
          table.columns = columns.
            select{|hash|table.match?(hash)}.
            map{|hash|Column.new(self, table, hash)}
        end
        
        result.each do |table|
          imported_keys = result_set_to_hash_array(md.getImportedKeys(catalog, schema_pattern, table.name))
          table.imported_keys = imported_keys.
            map{|hash|Key.new(self, table, hash)}
          table.imported_keys.each do |key|
            key.pktable = name_to_table[key.pktable_name]
            if key.pktable
              key.pktable.exported_keys ||= []
              key.pktable.exported_keys << key
            end
          end
        end
        
        @tables[key] = result
      end
      result
    end
  end
end
